<?php

if ( !defined( 'BASEPATH' ) ) exit( 'No direct script access allowed' );

/**
 * 继承CI的Form_validation类在其基础上增加令牌
 */
class MY_Form_validation extends CI_Form_validation
{

    /**
     * 令牌键值
     * @var string
     */
    var $key = 'token';

    /**
     * 表单令牌有效时间(秒)
     * @abstract 如果某些表单需要限制输入时间，则设置该值，为0则不限制
     * @var int 秒
     */
    var $token_validtime = 0;

    /**
     * 调试模式
     * @var bool
     */
    var $debug = true;

    /**
     * CI对象
     * @var <type>
     */
    var $_CI;

    public function __construct()
    {
        parent::__construct();
        $this->_CI = MY_Controller::get_instance();
        //如果配置没有填写encryption_key
        $encryption_key = config_item( 'encryption_key' );
        if ( empty( $encryption_key ) ) $this->_CI->config->set_item( 'encryption_key', $this->key );
        //如果没有装载session
        if ( !isset( $this->_CI->session ) ) $this->_CI->load->library( 'session' );
    }

    /**
     * 设置令牌有效时间
     * @param int $second 秒数
     */
    public function set_token_validtime( $second )
    {
        $this->token_validtime = intval( $second );
    }

    /**
     * 获取表单令牌有效时间
     * @return int 秒数
     */
    public function get_token_validtime()
    {
        return $this->token_validtime;
    }

    /**
     * 验证表单令牌是否合法
     * @return bool
     */
    public function valid_token()
    {
        if ( $this->debug ) return true;
        //获取session中的hash
        $source_hash = $this->_CI->session->userdata( $this->key );
        if ( empty( $source_hash ) ) return false;

        //判断是否超时
        if ( $this->is_token_exprie() ) return false;

        //提交的hash
        $post_formhash = $this->_CI->input->post( $this->key );
        if ( empty( $post_formhash ) ) return false;

        if ( md5( $source_hash ) == $post_formhash )
        {
            $this->_CI->session->unset_userdata( $this->key );
            return true;
        }
        return false;
    }

    /**
     * 生成表单令牌（连同input元素）
     * @return string
     */
    public function create_token( $output = false )
    {
        $code = time() . '|' . $this->get_random( 5 );
        $this->_CI->session->set_userdata( $this->key, $code );
        $result = function_exists( 'form_hidden' ) ? form_hidden( $this->key, md5( $code ) ) : "<input type=\"hidden\" name=\"{$this->key}\" value=\"" . md5( $code ) . "\" />";
        if ( $output )
        {
            echo $result;
        }
        else
        {
            return $result;
        }
    }

    /**
     * 获取随机数（自己可以扩展）
     * @param int $number 上限
     * @return string
     */
    public function get_random( $number )
    {
        return rand( 0, $number );
    }

    /**
     * 判断表单令牌是否过期
     * @return bool
     */
    public function is_token_exprie()
    {
        if ( empty( $this->token_validtime ) ) return false;
        $token = $this->_CI->session->userdata( $this->key );
        if ( empty( $token ) ) return false;
        $create_time = array_shift( explode( '|', $token ) );
        return ( time() - $create_time > $this->token_validtime );
    }

    /**
     * Executes the Validation routines
     *
     * @access	private
     * @param	array
     * @param	array
     * @param	mixed
     * @param	integer
     * @return	mixed
     */
    function _execute( $row, $rules, $postdata = NULL, $cycles = 0 )
    {
        // If the $_POST data is an array we will run a recursive call
        if ( is_array( $postdata ) )
        {
            foreach ( $postdata as $key => $val )
            {
                $this->_execute( $row, $rules, $val, $cycles );
                $cycles++;
            }

            return;
        }

        // --------------------------------------------------------------------
        // If the field is blank, but NOT required, no further tests are necessary
        $callback = FALSE;
        if ( !in_array( 'required', $rules ) AND is_null( $postdata ) )
        {
            // Before we bail out, does the rule contain a callback?
            if ( preg_match( "/(callback_\w+)/", implode( ' ', $rules ), $match ) )
            {
                $callback = TRUE;
                $rules = (array( '1' => $match[1] ));
            }
            else
            {
                return;
            }
        }

        // --------------------------------------------------------------------
        // Isset Test. Typically this rule will only apply to checkboxes.
        if ( is_null( $postdata ) AND $callback == FALSE )
        {
            if ( in_array( 'isset', $rules, TRUE ) OR in_array( 'required', $rules ) )
            {
                // Set the message type
                $type = (in_array( 'required', $rules )) ? 'required' : 'isset';

                if ( !isset( $this->_error_messages[$type] ) )
                {
                    if ( FALSE === ($line = $this->CI->lang->line( $type )) )
                    {
                        $line = 'The field was not set';
                    }
                }
                else
                {
                    $line = $this->_error_messages[$type];
                }

                // Build the error message
                $message = sprintf( $line, $this->_translate_fieldname( $row['label'] ) );

                // Save the error message
                $this->_field_data[$row['field']]['error'] = $message;

                if ( !isset( $this->_error_array[$row['field']] ) )
                {
                    $this->_error_array[$row['field']] = $message;
                }
            }

            return;
        }

        // --------------------------------------------------------------------
        // Cycle through each rule and run it
        foreach ( $rules As $rule )
        {
            $_in_array = FALSE;

            // We set the $postdata variable with the current data in our master array so that
            // each cycle of the loop is dealing with the processed data from the last cycle
            if ( $row['is_array'] == TRUE AND is_array( $this->_field_data[$row['field']]['postdata'] ) )
            {
                // We shouldn't need this safety, but just in case there isn't an array index
                // associated with this cycle we'll bail out
                if ( !isset( $this->_field_data[$row['field']]['postdata'][$cycles] ) )
                {
                    continue;
                }

                $postdata = $this->_field_data[$row['field']]['postdata'][$cycles];
                $_in_array = TRUE;
            }
            else
            {
                $postdata = $this->_field_data[$row['field']]['postdata'];
            }

            // --------------------------------------------------------------------
            // Is the rule a callback?
            $callback = FALSE;
            if ( substr( $rule, 0, 9 ) == 'callback_' )
            {
                $rule = substr( $rule, 9 );
                $callback = TRUE;
            }

            // Strip the parameter (if exists) from the rule
            // Rules can contain a parameter: max_length[5]
            $param = FALSE;
            if ( preg_match( "/(.*?)\[(.*)\]/", $rule, $match ) )
            {
                $rule = $match[1];
                $param = $match[2];
            }

            // Call the function that corresponds to the rule
            if ( $callback === TRUE )
            {
                if ( !method_exists( $this->_CI, $rule ) )
                {
                    continue;
                }

                // Run the function and grab the result
                $result = $this->_CI->$rule( $postdata, $param );

                // Re-assign the result to the master data array
                if ( $_in_array == TRUE )
                {
                    $this->_field_data[$row['field']]['postdata'][$cycles] = (is_bool( $result )) ? $postdata : $result;
                }
                else
                {
                    $this->_field_data[$row['field']]['postdata'] = (is_bool( $result )) ? $postdata : $result;
                }

                // If the field isn't required and we just processed a callback we'll move on...
                if ( !in_array( 'required', $rules, TRUE ) AND $result !== FALSE )
                {
                    continue;
                }
            }
            else
            {
                if ( !method_exists( $this, $rule ) )
                {
                    // If our own wrapper function doesn't exist we see if a native PHP function does.
                    // Users can use any native PHP function call that has one param.
                    if ( function_exists( $rule ) )
                    {
                        $result = $rule( $postdata );

                        if ( $_in_array == TRUE )
                        {
                            $this->_field_data[$row['field']]['postdata'][$cycles] = (is_bool( $result )) ? $postdata : $result;
                        }
                        else
                        {
                            $this->_field_data[$row['field']]['postdata'] = (is_bool( $result )) ? $postdata : $result;
                        }
                    }
                    else
                    {
                        log_message( 'debug', "Unable to find validation rule: " . $rule );
                    }

                    continue;
                }

                $result = $this->$rule( $postdata, $param );

                if ( $_in_array == TRUE )
                {
                    $this->_field_data[$row['field']]['postdata'][$cycles] = (is_bool( $result )) ? $postdata : $result;
                }
                else
                {
                    $this->_field_data[$row['field']]['postdata'] = (is_bool( $result )) ? $postdata : $result;
                }
            }

            // Did the rule test negatively?  If so, grab the error.
            if ( $result === FALSE )
            {
                if ( !isset( $this->_error_messages[$rule] ) )
                {
                    if ( FALSE === ($line = $this->CI->lang->line( $rule )) )
                    {
                        $line = 'Unable to access an error message corresponding to your field name.';
                    }
                }
                else
                {
                    $line = $this->_error_messages[$rule];
                }

                // Is the parameter we are inserting into the error message the name
                // of another field?  If so we need to grab its "field label"
                if ( isset( $this->_field_data[$param] ) AND isset( $this->_field_data[$param]['label'] ) )
                {
                    $param = $this->_translate_fieldname( $this->_field_data[$param]['label'] );
                }

                // Build the error message
                $message = sprintf( $line, $this->_translate_fieldname( $row['label'] ), $param );

                // Save the error message
                $this->_field_data[$row['field']]['error'] = $message;

                if ( !isset( $this->_error_array[$row['field']] ) )
                {
                    $this->_error_array[$row['field']] = $message;
                }

                return;
            }
        }
    }

}